import RNFS, { DownloadBeginCallbackResult } from 'react-native-fs';
import moment from 'moment';
import fetch from 'rnx-fetch';

import { DownloadVideoClass, TSItem, DownloadVideoObject } from './types';
import {
  VIDEO_TYPE,
  VIDEO_TYPE_DESC,
  DOWNLOAD_PATH,
  M3U8_FAIL_LIMIT,
  MOST_TASK_NUM,
  GET_VIDEO_TOKEN_PROD,
  GET_VIDEO_TOKEN_DEV
} from './constant';
import { DEBUG } from '../../constant';
import { getUserInfo } from '../../store/user';
import Log from '../../common/log';

const TAG = 'Download Log';

export default class DownloadVideo {
  public id: number | string; // id

  public title: string; // title

  public type: VIDEO_TYPE; // 视频type -ld -sd -hd

  public url: string; // m3u8的url

  public size: number; // 预计总文件大小

  public serverDir: string; // 服务器路径 -m3u8和ts文件所在目录

  public folderName: string; // 本地创建文件夹名 -与m3u8同名 -eg: c1c102cbf2ae973811c782720d49120e

  public tsList: TSItem[]; // ts文件列表

  public keyUrl: string; // 密钥地址

  public progress: number; // 总下载进度

  public indexHasDone: boolean; // m3u8索引是否下载完成

  public keyHasDone: boolean; // 密钥是否下载完成

  public m3u8Path: string; // 本地m3u8路径

  public parentId: number; // 父级id

  public parentTitle: string; // 父级title

  public isPaused: boolean; // 是否暂停

  public isFailed: boolean; // 是否失败

  public failedCount: number; // 失败计数器

  public startTime: string; // 下载开始时间

  public continueTime: Date; // 下载继续时间 用于计算下载时间

  public duration: number; // 下载时间

  public source: string; // 来源

  public onSuccess: (data: DownloadVideoObject) => void; // 下载成功回调

  public onProgress: (data: DownloadVideoObject) => void; // 进度回调 -m3u8 ts key下载成功时调用

  public onDelete: (data: DownloadVideoObject) => void; // 删除下载任务回调

  public onStart: (data: DownloadVideoObject) => void; // 开始下载任务回调

  public onPause: (data: DownloadVideoObject) => void; // 暂停下载任务回调

  public onFail: (data: DownloadVideoObject) => void; // 下载失败回调

  public downloadQueue: string[]; // 下载ts队列

  public startDownloadAllTs: () => void; // 开始下载全部ts

  public stopDownloadAllTs: () => void; // 停止下载全部ts

  public constructor(data: DownloadVideoClass) {
    const {
      id,
      title,
      type,
      url,
      size,
      serverDir,
      folderName,
      tsList,
      keyUrl,
      progress,
      indexHasDone,
      keyHasDone,
      m3u8Path,
      parentId,
      parentTitle,
      isPaused,
      startTime,
      duration,
      source,
      onSuccess,
      onProgress,
      onDelete,
      onStart,
      onPause,
      onFail
    } = data;

    this.id = id;
    this.title = title;
    this.type = type;
    this.url = url;
    this.size = size;
    this.parentId = parentId;
    this.parentTitle = parentTitle;
    this.serverDir = serverDir;
    this.folderName = folderName;
    this.tsList = tsList;
    this.keyUrl = keyUrl;
    this.progress = progress;
    this.indexHasDone = indexHasDone;
    this.keyHasDone = keyHasDone;
    this.m3u8Path = m3u8Path;
    this.isPaused = isPaused;
    this.isFailed = false;
    this.failedCount = 0;
    this.startTime = startTime || moment().format('YYYY-MM-DD HH:mm:ss.SSS');
    this.continueTime = new Date();
    this.duration = duration;
    this.source = source;

    this.onSuccess = onSuccess;
    this.onProgress = onProgress;
    this.onDelete = onDelete;
    this.onStart = onStart;
    this.onPause = onPause;
    this.onFail = onFail;

    this.downloadM3U8 = this.downloadM3U8.bind(this);
    this.readM3U8 = this.readM3U8.bind(this);
    this.downloadKey = this.downloadKey.bind(this);
    this.downloadTS = this.downloadTS.bind(this);
    this.getIsFinished = this.getIsFinished.bind(this);
    this.updateProgress = this.updateProgress.bind(this);
    this.start = this.start.bind(this);
    this.pause = this.pause.bind(this);
    this.delete = this.delete.bind(this);
    this.findLatestTs = this.findLatestTs.bind(this);
    this.getVideoToken = this.getVideoToken.bind(this);

    this.startDownloadAllTs = (): void => {
      this.tsList.forEach((item): void => {
        if (!item.hasDone && !this.downloadQueue.includes(item.tsName)) {
          this.downloadQueue.push(item.tsName);
          this.downloadTS(item);
        }
      });
    };

    this.stopDownloadAllTs = (): void => {
      const remainTasks = this.downloadQueue.splice(
        MOST_TASK_NUM,
        this.downloadQueue.length - MOST_TASK_NUM
      );

      this.tsList.forEach((item): void => {
        if (remainTasks.includes(item.tsName)) {
          RNFS.stopDownload(item.jobId);
        }
      });
    };

    this.downloadQueue = [];
  }

  public start(): void {
    this.isPaused = false;
    this.isFailed = false;
    this.onStart(this);
    if (this.indexHasDone) {
      setTimeout((): void => {
        while (this.downloadQueue.length < MOST_TASK_NUM) {
          if (this.isPaused || this.isFailed) {
            break;
          }
          const latestTs = this.findLatestTs();
          if (latestTs) {
            this.downloadQueue.push(latestTs.tsName);
            this.downloadTS(latestTs);
          } else {
            break;
          }
        }
      }, 1000);

      this.downloadKey();
    } else {
      this.downloadM3U8();
    }
  }

  public pause(): void {
    this.isPaused = true;

    this.tsList.forEach((item): void => {
      if (!item.hasDone && item.jobId) {
        RNFS.stopDownload(item.jobId);
      }
    });
    this.duration += new Date().getTime() - this.continueTime.getTime();

    this.onPause(this);
  }

  public delete(): void {
    this.pause();
    RNFS.unlink(`${DOWNLOAD_PATH}/${this.folderName}`);
    this.duration += new Date().getTime() - this.continueTime.getTime();
    this.onDelete(this);
  }

  // 下载m3u8
  private async downloadM3U8(): Promise<void> {
    if (this.isPaused || this.isFailed) {
      return;
    }

    const reload = this.downloadM3U8;

    const downloadPath = `${DOWNLOAD_PATH}/${this.folderName}`; // videos/1/c1c102cbf2ae973811c782720d49120e
    const m3u8Path = `${downloadPath}/${VIDEO_TYPE_DESC[this.type]}.m3u8`; // videos/1/c1c102cbf2ae973811c782720d49120e/sd.m3u8
    this.m3u8Path = m3u8Path;

    await RNFS.mkdir(downloadPath);

    const options = {
      fromUrl: this.url,
      toFile: m3u8Path,
      background: true,
      connectionTimeout: 8000,
      readTimeout: 8000,
      begin: (res: DownloadBeginCallbackResult): void => {
        const { jobId } = res;

        if (this.isPaused || this.isFailed) {
          RNFS.stopDownload(jobId);
        }
      }
    };
    const ret = RNFS.downloadFile(options);
    ret.promise
      .then((res): void => {
        const { statusCode } = res;
        if (statusCode === 200) {
          console.log('RUA 下载m3u8成功', res, this.folderName);
          Log.i(
            TAG,
            JSON.stringify({
              event: 'm3u8DownloadSuccess',
              name: this.folderName,
              res
            })
          );
          this.failedCount = 0;
          this.readM3U8();
        } else {
          console.log('RUA 下载m3u8失败', this.folderName, res);
          Log.i(
            TAG,
            JSON.stringify({
              event: 'm3u8DownloadFailed',
              name: this.folderName,
              res
            })
          );
          this.failedCount += 1;
          if (this.failedCount === M3U8_FAIL_LIMIT) {
            this.isFailed = true;
            this.onFail(this);
          }
          if (!this.isPaused && !this.isFailed) {
            reload();
          }
        }
      })
      .catch((err): void => {
        console.log('RUA 下载m3u8失败', this.folderName, err);
        this.failedCount += 1;
        if (this.failedCount === M3U8_FAIL_LIMIT) {
          this.isFailed = true;
          this.onFail(this);
        }
        if (!this.isPaused && !this.isFailed) {
          reload();
        }
      });
  }

  // 读取m3u8
  private async readM3U8(): Promise<void> {
    const m3u8Content = await RNFS.readFile(this.m3u8Path);

    const stringList = m3u8Content.split('\n');

    stringList.forEach((item): void => {
      if (item.includes('#EXT-X-KEY')) {
        this.keyUrl = item
          .split('URI=')[1]
          .replace(/"/g, '')
          .replace('\r', '');
      } else if (item.includes('.ts')) {
        const tsName = item.replace('\r', '');
        const index = this.tsList.findIndex(
          (ts): boolean => ts.tsName === tsName
        );
        if (index === -1) {
          this.tsList.push({
            tsName,
            hasDone: false,
            jobId: 0
          });
        }
      }
    });

    this.downloadKey();
    while (this.downloadQueue.length < MOST_TASK_NUM) {
      const latestTs = this.findLatestTs();
      if (this.isPaused || this.isFailed) {
        break;
      }
      if (latestTs) {
        this.downloadQueue.push(latestTs.tsName);
        this.downloadTS(latestTs);
      } else {
        break;
      }
    }
    // m3u8进度更新
    this.indexHasDone = true;
    this.updateProgress();
  }

  // 下载key
  private async downloadKey(): Promise<void> {
    if (this.isPaused || this.isFailed) {
      return;
    }

    const reload = this.downloadKey;

    try {
      // 带parentId后缓存路径不一样，需要特殊处理
      const videoTokenFile = await this.getVideoToken(
        `${DEBUG ? GET_VIDEO_TOKEN_DEV : GET_VIDEO_TOKEN_PROD}${
          this.parentId ? this.folderName.split('/')[1] : this.folderName
        }.m3u8`
      );

      const videoTokenText = await videoTokenFile.text();

      const stringList = videoTokenText.split('\n');

      const videoToken = stringList[2].split('m3u8?')[1];

      const keyName = `${VIDEO_TYPE_DESC[this.type]}.key`;
      const keyPath = `${DOWNLOAD_PATH}/${this.folderName}/${keyName}`;

      const options = {
        fromUrl: `${this.keyUrl}&${videoToken}`,
        toFile: `${keyPath}`,
        background: true,
        connectionTimeout: 8000,
        readTimeout: 8000,
        begin: (res: DownloadBeginCallbackResult): void => {
          const { jobId } = res;

          if (this.isPaused || this.isFailed) {
            RNFS.stopDownload(jobId);
          }
        }
      };

      const ret = RNFS.downloadFile(options);
      await ret.promise
        .then((res): void => {
          const { statusCode } = res;
          if (statusCode === 200) {
            console.log('RUA key 下载成功', this.folderName);
          } else {
            console.log('RUA key 下载失败', this.folderName, res);
            if (!this.isPaused && !this.isFailed) {
              reload();
            }
          }
        })
        .catch((err): void => {
          console.log('RUA key 下载失败', this.folderName, err);
          if (!this.isPaused && !this.isFailed) {
            reload();
          }
        });

      const m3u8Content = await RNFS.readFile(this.m3u8Path);

      // Android10 写文件不会覆盖，需要删除重写
      await RNFS.unlink(this.m3u8Path);
      await RNFS.writeFile(
        this.m3u8Path,
        m3u8Content.replace(this.keyUrl, keyName)
      );

      this.keyHasDone = true;
      this.updateProgress();
    } catch (err) {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'videoKeyDownloadFail',
          url: `${DEBUG ? GET_VIDEO_TOKEN_DEV : GET_VIDEO_TOKEN_PROD}${
            this.parentId ? this.folderName.split('/')[1] : this.folderName
          }.m3u8`
        })
      );
    }
  }

  // 下载ts
  private downloadTS(tsItem: TSItem): void {
    if (this.isPaused || this.isFailed) {
      return;
    }
    const reload = this.downloadTS;

    Log.i(
      TAG,
      JSON.stringify({
        event: 'startDownloadTs',
        tsName: tsItem.tsName,
        m3u8: this.folderName
      })
    );

    const { tsName } = tsItem;

    const options = {
      fromUrl: `${this.serverDir}/${tsName}`,
      toFile: `${DOWNLOAD_PATH}/${this.folderName}/${tsName}`,
      background: true,
      discretionary: true,
      connectionTimeout: 8000,
      readTimeout: 8000,
      begin: (res: DownloadBeginCallbackResult): void => {
        const { jobId } = res;
        const index = this.tsList.findIndex(
          (item): boolean => item.tsName === tsName
        );
        if (index !== -1) {
          this.tsList[index].jobId = jobId;
        }

        if (this.isPaused || this.isFailed) {
          RNFS.stopDownload(jobId);
        }
      }
    };

    const ret = RNFS.downloadFile(options);
    ret.promise
      .then((res): void => {
        const { statusCode } = res;
        if (statusCode === 200) {
          console.log(
            'RUA 下载ts成功',
            this.folderName,
            tsName,
            this.isPaused,
            this.isFailed
          );
          Log.i(
            TAG,
            JSON.stringify({
              event: 'tsDownloadSuccess',
              tsName,
              m3u8: this.folderName
            })
          );
          this.failedCount = 0;
          const index = this.tsList.findIndex(
            (item): boolean => item.tsName === tsName
          );
          if (index !== -1) {
            this.tsList[index].hasDone = true;
          }
          this.updateProgress();
          if (!this.isPaused && !this.isFailed) {
            const i = this.downloadQueue.findIndex(
              (item): boolean => item === tsName
            );
            if (i !== -1) {
              this.downloadQueue.splice(i, 1);
            }
            if (this.isPaused || this.isFailed) {
              return;
            }

            const latestTs = this.findLatestTs();
            Log.i(
              TAG,
              JSON.stringify({
                event: 'tsSuccessSubscription',
                tsName,
                m3u8: this.folderName,
                latestTs: JSON.stringify(latestTs)
              })
            );
            if (latestTs) {
              this.downloadQueue.push(latestTs.tsName);
              this.downloadTS(latestTs);
            }
          }
        } else {
          console.log('RUA 下载ts失败', this.folderName, tsName, res);
          this.failedCount += 1;
          if (this.failedCount === this.tsList.length * 2) {
            this.isFailed = true;
            this.onFail(this);
          }
          if (!this.isPaused && !this.isFailed) {
            reload(tsItem);
          }
        }
      })
      .catch((err): void => {
        console.log('RUA 下载ts失败', this.folderName, tsName, err);
        Log.i(
          TAG,
          JSON.stringify({
            event: 'tsDownloadFailed',
            tsName,
            err
          })
        );
        this.failedCount += 1;
        if (this.failedCount === this.tsList.length * 2) {
          this.isFailed = true;
          this.onFail(this);
        }
        if (!this.isPaused && !this.isFailed) {
          reload(tsItem);
        }
      });
  }

  private getIsFinished(): boolean {
    const tsHaveDone = this.tsList.every((item): boolean => item.hasDone);
    return this.indexHasDone && this.keyHasDone && tsHaveDone;
  }

  private updateProgress(): void {
    const total = this.tsList.length + 2;
    const tsFinishedNum = this.tsList.reduce((finishedNum, current): number => {
      const val = current.hasDone ? 1 : 0;
      return finishedNum + val;
    }, 0);
    const indexFinishedNum = this.indexHasDone ? 1 : 0;
    const keyFinishedNum = this.keyHasDone ? 1 : 0;
    this.progress = Number(
      ((tsFinishedNum + indexFinishedNum + keyFinishedNum) / total).toFixed(2)
    );

    this.onProgress(this);

    console.log('RUA progress', this.progress);

    if (this.getIsFinished()) {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'downloadVideoSuccess',
          m3u8: this.folderName
        })
      );
      console.log('RUA success', this.folderName);
      this.isPaused = true;
      this.duration += new Date().getTime() - this.continueTime.getTime();
      this.onSuccess(this);
    }
  }

  private findLatestTs(): TSItem | undefined {
    const latestTs = this.tsList.find(
      (item): boolean =>
        !item.hasDone && !this.downloadQueue.includes(item.tsName)
    );
    return latestTs;
  }

  private getVideoToken = (url: string): Promise<any> => {
    const { userId, token } = getUserInfo();

    // const clientInfo = {
    //   userId,
    //   appName: APP_NAME,
    //   appVersion: APP_VERSION,
    //   channelName: CHANNEL,
    //   deviceId: DEVICE_ID,
    //   platInfo: OS_TYPE
    // };
    return fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'audio/x-mpegurl',
        token: 'offline-aikid'
        // clientInfo: JSON.stringify(clientInfo)
      }
    });
  };
}
